---
sidebar_position: 4
title: Request
sidebar_label: Request
---

[Read the API Reference »](/docs/api/core/Classes/Request.mdx)

The `Request` class is the cornerstone of Hyper Fetch's data-fetching system. It provides a **powerful, type-safe way to
define, configure, and manage API requests** throughout your application. By encapsulating all the details needed to
perform a request—such as endpoint, method, parameters, payload, and behavior - `Request` ensures consistency,
predictability, and flexibility in how you interact with remote data sources.

With a strict and predictable data structure, `Request` makes it easy to serialize, persist, and rehydrate request
states, enabling advanced features like offline support, caching, and queueing. Its design, especially when paired with
TypeScript, helps you catch errors early and build robust, maintainable data flows.

---

:::tip Purpose

1. Requests role is to create **consistent API request templates**.
2. All requests "templates" are **reusable** and share the data structure.
3. They behave in the same way no matter what the adapter is, to improve reusability.
4. Request class **holds instructions** on how requests should be sent, queued, retried, or cancelled.
5. Responses are **typed** and **structured** to make it easy to work with the data.

:::

---

## Initialization

Request should be initialized from the [Client](/docs/core/client) instance with `createRequest` method. This passes a
shared reference to the place that manages communication in the application.

```tsx title="Example of Request initialization"
import { client } from "./client";

const getUser = client.createRequest()({
  endpoint: "/users/:userId",
});
```

```tsx live  title="Simple fetch" size="small"
const response = await getUser.send({ params: { userId: 1 } });

console.log(response);
```

:::caution TypeScript Currying Note

The `createRequest` method currently employs a currying pattern to facilitate auto-generated TypeScript types for
endpoint strings. This is a temporary approach until
[this specific TypeScript issue](https://github.com/microsoft/TypeScript/issues/10571) is resolved, which will allow for
a more direct typing mechanism.

:::

---

## Quick Start

Here is a quick start guide to help you get started with Requests.

### Fetching

There are two main methods to send a request with Hyper Fetch - `send(options)` and `exec(options)`.

```mermaid
    timeline
        title Request Lifecycle
          1. Pre-Request : Deduplication : Queueing : Interceptors
          2. Request : onRequestStart() : onUploadProgress() : onRequestEnd()
          3. Side Effects : onAbort() : onRemoved() : Stop() : Start()
          4. Response : onResponseStart() : onDownloadProgress() : onResponseEnd()
          5. Result : Interceptors : onSuccess() : onError() : onFinish()
```

1. #### `.send(options)`

You can perform a request with the `send(options)` method. This is the most common way to send a request with Hyper
Fetch. It triggers the request's lifecycle, including queueing, deduplication, and response handling, and returns the
server's response in a developer-friendly format.

```tsx
// Simple fetch
const { data, error } = await getNotes.setQueryParams({ sort: "age" }).send();

// Multiple chained methods
const { data, error } = await getCategory.setParams({ categoryId: 2 }).send();

// Passing options
const { data, error } = await getCategory.send({ params: { categoryId: 1 }, queryParams: { sort: "createdAt" } });
```

2. #### `.exec(options)`

This method is very similar to `send` but it ignores the built-in features - like for example deduplication or caching.
It is useful when you want to execute a request outside of the normal flow or when you want to bypass the cache
interactions - for example while using in SSR environments.

```tsx
const { data, error } = await getUser.exec();
```

<LinkCard
  type="guides"
  title="Fetching"
  description="Discover how Hyper Fetch handles request execution."
  to="/docs/guides/core/basics/fetching"
/>

---

### Request Options

Each request can be configured with options. We can specify things like `endpoint`, `method`, `caching strategy`,
`deduplication`, etc. This information is stored as a template for later use.

```ts
const request = client.createRequest()({
  // Here are some basic options
  // highlight-start
  endpoint: "/some-endpoint",
  method: "POST",
  deduplicate: true,
  // highlight-end
});
```

(@import core RequestOptionsType type=returns)

---

### Payload

When you want to send some data along with the request, you can use the `setPayload` method. This can be a json,
FormData or other format - depending on the adapter you use.

First we define request expecting proper type.

```ts
const postUser = client.createRequest<{
  response: UserModel;
  // highlight-start
  payload: UserPostDataType;
  // highlight-end
}>()({
  method: "POST",
  endpoint: "/users",
});

const postFile = client.createRequest<{
  // highlight-start
  payload: FormData;
  // highlight-end
}>()({
  method: "POST",
  endpoint: "/files",
});
```

```ts
// Regular data
const response = await postUser.setPayload({ name: "John", age: 18 }).send();
```

You can also pass payload directly to the `send` method.

```ts
const response = await postFile.send({ payload: data });
```

<LinkCard
  type="guides"
  title="Payload"
  description="Understand how to send payload data with your requests."
  to="/docs/guides/core/basics/payload"
/>

#### Payload with files

When you want to send a file, you can use the `FormData`.

```ts
// Form data
const data = new FormData();
const file = new File([], "file.txt");
data.append("file", file);

const response = await postFile.setPayload(data).send();
```

<LinkCard
  type="guides"
  title="Data Mapping Guide"
  description="Discover how to map and transform data effectively."
  to="/docs/guides/core/basics/data-mapping"
/>

---

### Parameters

Parameters are the URL parts used to identify a specific resource. In HyperFetch they are defined in the endpoint part
with `:` prefix. For example:

```ts
const getNote = client.createRequest()({
  endpoint: "/note/:noteId", // noteId is a parameter
});
const getCategory = client.createRequest()({
  endpoint: "/category/:categoryId", // categoryId is a parameter
});
const getCategoryNote = client.createRequest()({
  endpoint: "/category/:categoryId/note/:noteId", // categoryId and noteId are parameters
});
```

When you have properly prepared requests that expect parameters, you can add parameters using the `setParams` method. In
generic TypeScript, these parameters will match the endpoint parameters by using literal types and will require literal
types.

```tsx
getNote.setParams({ noteId: 1 });
getCategory.setParams({ categoryId: 2 });
getCategoryNote.setParams({ categoryId: 2, noteId: 1 });
```

You can also pass parameters directly to the `send` method.

```ts
const response = await getCategoryNote.send({ params: { categoryId: 1, noteId: 2 } });
```

<LinkCard
  type="guides"
  title="Parameters"
  description="Learn how to define and use route parameters in your requests."
  to="/docs/guides/core/basics/parameters"
/>

---

### Query parameters

Query parameters are used to filter or sort the data. In Http requests they are defined in the endpoint part with `?`
prefix. For example `?search=John&sort=age`.

We can set query params with the `setQueryParams` method.

```ts
const getUsers = client.createRequest<{
  // highlight-start
  queryParams?: { search?: string; sort?: string };
  // highlight-end
}>()({
  endpoint: "/users",
});
```

```tsx
const response = await getUsers.setQueryParams({ search: "John" }).send();
```

Note that we use `?:` prefix to make query params optional. We can make it required as a whole but also particular
params from the set.

:::note Required Query Params

You can make query params required by specifying them without `?:` prefix.

```ts
// All params are required before request is sent
const getUsers = client.createRequest<{
  // highlight-start
  queryParams: { search: string; sort: string };
  // highlight-end
}>()({
  endpoint: "/users",
});

// Typescript error - sort is required
// error-start
const response = await getUsers.setQueryParams({ search: "John" }).send();
// error-end

// Typescript error - search is required
// error-start
const response = await getUsers.setQueryParams({ sort: "age" }).send();
// error-end

// Typescript error - query params are required
// error-start
const response = await getUsers.send();
// error-end
```

:::

```tsx
getUsers.setQueryParams({ search: "John", sort: "age" });
```

The encoding type for arrays and other options can be set up in the Client. You can also provide your own encoding
logic.

```tsx
getUsers.setQueryParams({ search: "John", sort: "age" });
```

<LinkCard
  type="guides"
  title="Query Parameters"
  description="Explore how to set and manage query parameters for API requests."
  to="/docs/guides/core/basics/query-params"
/>

---

### Adapter Options

You can pass adapter options down with the request options.

```ts
const request = client.createRequest()({
  endpoint: "/some-endpoint",
  // highlight-start
  // Here are adapter options for the Http adapter (default one)
  options: { timeout: 1000, withCredentials: true },
  // highlight-end
});
```

:::info

Adapter options may vary depending on the adapter you use.

:::

---

## Methods

The `Request` class offers a suite of methods to configure and interact with a request instance. You can find a
comprehensive list of these methods and their detailed descriptions in the API reference.

<ShowMore>

(@import core Request type=methods&display=table)

</ShowMore>

<LinkCard
  type="api"
  title="Detailed Request API Methods"
  description="Explore all available methods, their parameters, and return values for the Request class."
  to="/docs/api/core/Classes/Request#methods"
/>

It is crucial to understand how these methods operate to ensure predictable behavior:

:::danger Methods Return Clones

Most methods on a `Request` instance (e.g., `setParams`, `setData`, `setHeaders`) do not modify the original request
instance in place. Instead, they return a **new, cloned instance** of the request with the specified modifications
applied. The original request instance remains unchanged.

Always use the returned new instance for subsequent operations or for sending the request. This immutable approach
ensures isolation between different request configurations and is fundamental to how Hyper Fetch manages request states
for features like caching and queueing.

:::

```tsx
const initialRequest = getUser;
// ❌ WRONG - Original request (getUser) remains unchanged
initialRequest.setParams({ userId: 1 }); // setParams returns a new request instance
// 'initialRequest' still refers to the original getUser without params.

// Sends the original request without params, likely leading to an error.
const { data, error } = await initialRequest.send();
```

```tsx
// ✅ CORRECT - Use the returned (cloned) instance
const originalRequest = getUser;

// Assign the new, cloned request with parameters to a new variable
const requestWithParams = originalRequest.setParams({ userId: 1 });

// Send the correctly configured request
const { data, error } = await requestWithParams.send(); // Success!

// ✅ Also correct (method chaining creates and passes clones implicitly)
const { data: chainedData, error: chainedError } = await getUser
  .setParams({ userId: 1 }) // .setParams returns a clone
  .send(); // .send operates on the final configured clone
```

---

## Keys

Each `Request` instance is assigned several unique identifiers, known as **keys**. These keys are essential for internal
mechanisms—such as caching, queueing, request cancellation, deduplication, effect management and many more. Think of
them as "fingerprints" that help Hyper Fetch track, manage, and use for assigning side-effects to requests.

:::note Keys are generated automatically

Keys are generated automatically based on the request's endpoint, method, and parameters. Currently there are two types
of the generation mechanisms - simple and complex keys. Complex keys include query params in the generation - simple
keys include only endpoint and method.

:::

### `queryKey`

General identifier for the request, used for query management (e.g., refetching, optimistic updates, invalidation).

```ts
const key = request.queryKey;
```

Set custom key with `setQueryKey` method.

```ts
const customRequest = request.setQueryKey("custom-key");
```

<LinkCard
  type="docs"
  title="More about queryKey"
  description="Learn more about queryKey."
  to="/docs/core/dispatcher#querykey"
/>

---

### `cacheKey`

Determines how the response is stored and retrieved from the cache. Ensures correct data is cached and served.

```ts
const key = request.cacheKey;
```

Set custom key with `setCacheKey` method.

```ts
const customRequest = request.setCacheKey("custom-key");
```

<LinkCard
  type="docs"
  title="More about cacheKey"
  description="Learn more about cacheKey."
  to="/docs/core/cache#cachekey"
/>

---

### `abortKey`

Identifies and manages ongoing requests for cancellation (e.g., aborting in-flight requests).

```ts
const key = request.abortKey;
```

Set custom key with `setAbortKey` method.

```ts
const customRequest = request.setAbortKey("custom-key");
```

<LinkCard
  type="docs"
  title="More about abortKey"
  description="Learn more about abortKey."
  to="/docs/core/managers#abortkey"
/>

---

#### Generation mechanism

By default, these keys are auto-generated based on the request's endpoint, method, and parameters. This ensures that
each unique request configuration is tracked separately. Your can override this mechanism by providing your own
implementation.

```tsx
import { client } from "./api";

client.setCacheKeyMapper((request) => {
  if (request.requestOptions.endpoint === "/users/:userId") {
    return `CUSTOM_CACHE_KEY_${request.params?.userId || "unknown"}`;
  }
});
```

#### Why override keys?

In most cases, the default keys are sufficient. However, you may want to override them for advanced scenarios, such as:

- **Custom cache strategies**: Grouping multiple endpoints under a single cache key.
- **Manual request deduplication**: Treating different requests as identical for deduplication or cancellation.
- **Abort groups**: Being able to abort multiple requests at once.

#### Overriding keys

You can override any key using the corresponding method:

```ts
const customRequest = request.setQueryKey("custom-key");
const customCacheRequest = request.setCacheKey("my-cache-key");
const abortableRequest = request.setAbortKey("my-abort-key");
const effectLinkedRequest = request.setEffectKey("my-effect-key");
```

> **Note:** Overriding keys is an advanced feature. Make sure you understand the implications for caching,
> deduplication, and request management.

---

## Features

The `Request` class is packed with features to streamline your data fetching and management. Here are some key
capabilities:

1. ### Cancellation

Cancellation allows you to abort an in-progress request before it completes. This is useful in scenarios where the
result of a request is no longer needed, such as when a user navigates away from a page or changes a filter before the
previous request finishes. Aborting a request helps prevent unnecessary processing and side effects from outdated
responses.

1. #### Manual cancellation

Triggered by the `.abort()` request method, allow us to stop the request execution.

```tsx live title="Manual cancellation"
import { getUser } from "./api";

// Start the request
const response = getUser.send();

setTimeout(() => {
  // Cancel the request by its abortKey
  getUser.abort();
  // Show a toast notification
  toast({ title: "Cancelled", message: "This request was cancelled manually" });
}, 1000);
```

2. #### Automatic cancellation

It is automatically executed when we send two identical requests at the same time. Older request will be cancelled and
the new one will be sent.

```tsx live title="Automatic cancellation"
import { getUser } from "./api";

const cancelableRequest = getUser.setCancelable(true);

cancelableRequest.send();

setTimeout(() => {
  cancelableRequest.send();
}, 1000);
```

<LinkCard
  type="guides"
  title="Cancellation Guide"
  description="Learn how to cancel requests and manage their lifecycle."
  to="/docs/guides/core/advanced/cancellation"
/>

---

2. ### Queueing

Queueing ensures that requests are sent one after another, rather than in parallel. When enabled, each request waits for
the previous one to finish before starting. This is important for operations that must be performed in order, or when
interacting with APIs that require serialized access.

```tsx live title="Queueing"
import { postFile } from "./api";

// Queue requests to be sent one-by-one
const queuedRequest = postFile.setQueued(true);
queuedRequest.send();
// This will wait until the previous with th same queryKey one finishes
queuedRequest.send();
```

<LinkCard
  type="guides"
  title="Queueing"
  description="Understand how requests are queued and processed in sequence."
  to="/docs/guides/core/advanced/queueing"
/>

---

3. ### Offline Support

Offline support allows requests to be stored when the application is offline. These requests are automatically retried
once the network connection is restored. This feature is useful for applications that need to function reliably in
environments with intermittent connectivity.

```tsx live  title=Offline handling
import { client, getUser } from "./api";

// Simulate the connection loss
client.appManager.setOnline(false);

// Trigger the request being offline
getUser.setOffline(true).send();

// Return back online and the request will be resumed
setTimeout(() => {
  client.appManager.setOnline(true);
}, 2000);
```

<LinkCard
  type="guides"
  title="Offline Guide"
  description="Learn how to leverage offline capabilities for robust applications."
  to="/docs/guides/core/advanced/offline"
/>

---

4. ### Deduplication

Deduplication prevents multiple identical requests from being sent at the same time. If a request is already in
progress, additional requests with the same parameters will reuse the ongoing request and share its response. This
reduces redundant network traffic and ensures consistent results.

```tsx live
import { getUser } from "./api";

const getUserDeduplicated = getUser.setDeduplicate(true);

// First request
getUserDeduplicated.send();

// Second request (Deduplicated - it will never be triggered)
getUserDeduplicated.send();

// Third request (Deduplicated - it will never be triggered)
setTimeout(() => {
  getUserDeduplicated.send();
}, 500);
```

<LinkCard
  type="guides"
  title="Deduplication"
  description="Learn how to prevent duplicate requests using the deduplication feature."
  to="/docs/guides/core/advanced/deduplication"
/>

---

5. ### Authentication

Authentication integrates the request with the client's authentication mechanism. When enabled, authentication tokens or
credentials are automatically included with the request. This is necessary for accessing protected resources or APIs
that require user identity.

```tsx
// Add the onAuth interceptor
client.onAuth(({ request }) => {
  // Modify request - by adding the auth token to the headers
  request.setHeaders({
    Authorization: `Bearer ${auth.token}`,
  });

  return request;
});

// Authenticated request (uses Client's auth setup)
const authRequest = client.createRequest()({ endpoint: "/profile", auth: true });

// it will authenticate the request
authRequest.send();
```

<LinkCard
  type="guides"
  title="Authentication Guide"
  description="Learn about setting up authentication for your requests via the Client."
  to="/docs/guides/core/basics/authentication"
/>

---

6. ### Mapping

**Response mapping** allows you to transform the request payload before sending it, or the response data after receiving
it. This is useful for adapting data formats between your application and external APIs, or for preprocessing data for
validation or display.

```tsx
// Map response data to a new structure
const { data: mappedResponse } = await client
  .createRequest<{ response: { name: string } }>()({ endpoint: "/user" })
  .setResponseMapper((res) => ({ ...res, data: { displayName: res.data.name } }))
  .send();

console.log(mappedResponse); // { displayName: "John" }
```

<LinkCard
  type="guides"
  title="Response Mapping Guide"
  description="Discover how to map and transform data effectively."
  to="/docs/guides/core/basics/data-mapping"
/>

**Request mapping** provides hooks for custom transformations of payloads, requests, and responses. This allows for
complex data processing scenarios, such as chaining multiple transformations, integrating with third-party services, or
adapting to evolving API contracts.

```tsx
// Advanced mapping: transform payload before sending
const request = client
  .createRequest<{ payload: { name: string } }>()({ endpoint: "/advanced" })
  .setPayloadMapper((payload) => ({ ...payload, name: payload.name.toUpperCase() }));

// Will send the request with the payload { name: "JOHN" }
const mappedRequest = request.setPayload({ name: "john" }).send();
```

<LinkCard
  type="guides"
  title="Request Mapping Guide"
  description="Explore advanced data mapping techniques for complex scenarios."
  to="/docs/guides/core/advanced/mapping"
/>

---

7. ### Retries

Retries automatically resend a request if it fails due to network errors or server issues. You can configure the number
of retry attempts and the delay between them. This feature helps improve reliability in the face of transient errors.

```tsx
// Retry failed requests automatically
const retryRequest = client.createRequest()({ endpoint: "/unstable", retry: 2, retryTime: 1000 });
retryRequest.send(); // Will retry up to 2 times on failure
```

<LinkCard
  type="guides"
  title="Retries"
  description="Learn how to configure automatic retries for failed requests."
  to="/docs/guides/core/basics/retries"
/>

---

8. ### Error handling

Error handling provides mechanisms to detect and respond to errors that occur during the request lifecycle. You can
define callbacks for different error types, such as network failures, validation errors, or server responses, allowing
your application to handle failures appropriately.

```tsx
// Handle errors with onError callback
const errorRequest = client.createRequest()({ endpoint: "/fail" });

// Will return error without throwing it
const { error } = await errorRequest.send();

// You can also handle the error with callback
await errorRequest.send({
  onError: ({ response }) => {
    console.error("Request failed:", response.error);
  },
});
```

<LinkCard
  type="guides"
  title="Error handling"
  description="Understand the error handling mechanisms in Hyper Fetch requests."
  to="/docs/guides/core/basics/error-handling"
/>

---

9. ### Validation

Validation enables you to check the request data before it is sent, and the response data after it is received. This
helps ensure that only valid data is processed by your application, and that errors are caught early in the data flow.

```tsx
// Validate request data before sending
const validatedRequest = client
  .createRequest<{ payload: { age: number } }>()({ endpoint: "/validate" })
  .setRequestMapper((req) => {
    if (req.payload.age < 18) throw new Error("Must be 18+");
    return req;
  });
const { error } = await validatedRequest.setPayload({ age: 16 }).send();

console.log(error); // Error: Must be 18+
```

<LinkCard
  type="guides"
  title="Validation"
  description="Learn about validating request data before sending and response data upon receipt."
  to="/docs/guides/core/advanced/validation"
/>

---

## Lifecycle listeners

The `send()` method accepts an options object where you can define lifecycle callback functions. These callbacks allow
you to hook into various stages of the request's lifecycle—such as `onBeforeSent` (when the request finishes, regardless
of outcome), `onSuccess`, `onError`, `onStart`, `onProgress`, `onAbort`, and `onOfflineError`—to perform actions based
on the request's progress and outcome.

```tsx
await someRequest.send({
  onBeforeSent: ({ response, requestId }) => {
    console.log(`Request ${requestId} has settled. Final response:`, response);
    // Called right before the request is sent—useful for quickly accessing the requestId for tracking or logging
  },
  onStart: ({ requestId, request }) => {
    console.log(`Request ${requestId} has started.`);
    // Called when the request is started—useful for showing a loading indicator
  },
  onUploadProgress: ({ progress, timeLeft, sizeLeft, total, loaded, startTimestamp, request, requestId }) => {
    console.log(`Request ${requestId} upload progress:`, progress);
    // Called when the upload progress is updated—useful for showing a progress bar
  },
  onDownloadProgress: ({ progress, timeLeft, sizeLeft, total, loaded, startTimestamp, request, requestId }) => {
    console.log(`Request ${requestId} download progress:`, progress);
    // Called when the download progress is updated—useful for showing a progress bar
  },
  onResponse: ({ response, requestId }) => {
    console.log(`Request ${requestId} finished with data:`, response.data);
    // Called when the request succeeds—useful for processing successful data
  },
  onRemove: ({ request, requestId }) => {
    console.log(`Request ${requestId} removed.`);
    // Called when the request is removed—useful for cleaning up
  },
});
```

---

## TypeScript

When creating a request with `client.createRequest`, you can specify up to four generic types to ensure type safety and
enhance the developer experience throughout the request lifecycle:

- **`response`**: The expected type of the successful response data from the server.
- **`payload`**: The type of the data to be sent in the request body.
- **`error`**: The type for errors specific to this request, often used for local validation or business logic errors,
  complementing the global error type defined on the `Client`.
- **`queryParams`**: The expected structure and types for the request's query parameters.

```tsx
const request = client.createRequest<{
  response: { name: string };
  payload: { age: number };
  error: { message: string };
  queryParams: { search: string };
}>()({ endpoint: "/user" });

const { data, error } = await request.send();

console.log(data); // { name: "John" }
console.log(error); // undefined
```

:::warning Install Eslint plugin

To ensure type safety, we recommend installing the
[eslint-plugin-hyper-fetch](https://www.npmjs.com/package/eslint-plugin-hyper-fetch) plugin. It will help you catch type
errors early and build robust, maintainable data flows.

Learn how to install and configure the plugin in the [Eslint plugin guide](/docs/integrations/plugin-eslint).

:::

---

### Generics

Starting from `Hyper Fetch v7.0.0`, all the types for the `Client` class and the `createRequest` method are passed as
objects. This way, user can specify **only** what they want, in any order, in very a readable way.

```ts
const client = new Client<{ error: ErrorType }>({ url });

const getUsers = client.createRequest<{ response: ResponseType; queryParams: QueryParamsType }>()({
  endpoint: "/users",
});
```

:::caution

We firmly believe that it is more readable approach. Yet, it comes at a cost - currently, TypeScript does not handle
well the autosuggestions and type inference for the extended object-like generics
(https://github.com/microsoft/TypeScript/issues/28662). Fear not - the types are working correctly and we found a way to
make it work with our own eslint plugin.

For example, when writing a request, e.g.:

```ts
const postUser = client.createRequest<{ invalidGeneric: string }>()({ endpoint: "/" });
```

TypeScript will not throw an error if you use an invalid key like `invalidGeneric` that isn't part of our API. To catch
these issues early, we recommend using our custom ESLint plugin. This approach combines the benefits of type safety and
readability, while ensuring your code uses the correct API syntax.

:::
